package com.mic.yzzn.plc.core.internet;

import com.mic.yzzn.plc.common.control.datastructure.PLCDataStatus;
import com.mic.yzzn.plc.common.ins.datastructure.PLCInstructionTuple;
import com.mic.yzzn.plc.common.instructions.basic.PLCInstructionError;
import com.mic.yzzn.plc.common.instructions.basic.PLCRequestInstruction;
import com.mic.yzzn.plc.common.instructions.basic.PLCResponseInstruction;
import com.mic.yzzn.plc.common.instructions.request.*;
import com.mic.yzzn.plc.common.instructions.response.PLCDataRegisterReadResponse;
import com.mic.yzzn.plc.common.instructions.response.PLCMultipleReadResponse;
import com.mic.yzzn.plc.common.instructions.response.PLCSingleReadResponseInstruction;
import com.mic.yzzn.plc.common.toolUtils.PLCNumberTransform;
import org.apache.log4j.Logger;

import java.io.*;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;

/**
 * 负责与PLC交互的类,发送和接收指令,本类使用的是 TCP 的方式进行发送和接收
 */
public class TCPInstructionDispatcher implements InstructionDispatcher {
    /**
     * 端口号
     */
    private int port;
    /**
     * IP地址
     */
    private String IP;
    /**
     * 最大读取次数错误
     */
    int maxReadErrorTimes = 5;
    /**
     * 超时因子
     */
    public static int timeoutFactor = 5;

    private static Logger logger = Logger.getLogger(TCPInstructionDispatcher.class);

    @SuppressWarnings("unused")
    private TCPInstructionDispatcher() {

    }

    public TCPInstructionDispatcher(int port, String IP) {
        this.port = port;
        this.IP = IP;
    }

    /**
     * 发送单条指令
     *
     * @param instruction 请求指令
     * @return 响应信息的字符串
     */
    public PLCInstructionTuple sendInstruction(PLCRequestInstruction instruction, boolean ack, int timeoutUnit) {
        PLCInstructionTuple result = new PLCInstructionTuple();

        ArrayList<PLCRequestInstruction> instructions = new ArrayList<PLCRequestInstruction>();
        instructions.add(instruction);

        ArrayList<PLCInstructionTuple> response = sendInstructions(instructions, ack, timeoutUnit);

        if (response.size() > 0) {
            result = response.get(0);
        }

        System.out.println("Done");
        return result;
    }


    /**
     * 发送多条指令
     *
     * @param instructions 指令数组
     * @param ack          是否确认结果
     * @param timeoutUnit  单条指令等待的时间单位
     * @return
     */
    public ArrayList<PLCInstructionTuple> sendInstructions(ArrayList<PLCRequestInstruction> instructions, boolean ack, int timeoutUnit) {
        ArrayList<PLCInstructionTuple> result = new ArrayList<PLCInstructionTuple>();
        Socket socket = null;

        try {
            // 初始化Socket
            socket = new Socket(IP, port);
            socket.setSoTimeout(timeoutUnit * instructions.size() * timeoutFactor);

            // 取得输入输出流
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);

            for (int i = 0; i < instructions.size(); ++i) {
                PLCInstructionTuple tuple = new PLCInstructionTuple();
                PLCRequestInstruction instruction = instructions.get(i);
                tuple.requestInstruction = instruction;

                // 向PLC发送请求
                String response = "";
                if (ack) {
                    System.out.println("需要确认");
                    response = sendInstruction_fireAck(dataOutputStream, dataInputStream, instruction);
                } else {
                    System.out.println("不需要确认");
                    response = socket_Send(dataOutputStream, dataInputStream, instruction);
                }

                tuple.responseString = response;

                // 判断返回数据的正确性
                if (response.charAt(3) == '!') {
                    String errorCode = response.substring(4, 6);
                    int errorType = Integer.parseInt(errorCode);

                    switch (errorType) {
                        case PLCInstructionError.BASIC_PROGRAM_BBC_ERROR:
                            tuple.responseState = PLCDataStatus.PLC_DATA_STATUS_RESPONSE_BBC_ERROR;
                            break;

                        case PLCInstructionError.CORE_INNER_CONNECT_ERROR:
                            tuple.responseState = PLCDataStatus.PLC_DATA_STATUS_CONNECT_ERROR;
                            break;

                        case PLCInstructionError.CORE_INNER_NO_RESPONSE_ERROR:
                            tuple.responseState = PLCDataStatus.PLC_DATA_STATUS_RESPONSE_NULL_ERROR;
                            break;

                        case PLCInstructionError.CORE_INNER_CONFIRM_ERROR:
                            tuple.responseState = PLCDataStatus.PLC_DATA_STATUS_CONFIRM_ERROR;
                            break;

                        default:
                            tuple.responseState = PLCDataStatus.PLC_DATA_STATUS_ERROR;
                            break;
                    }
                } else {
                    tuple.responseState = PLCDataStatus.PLC_DATA_STATUS_OK;
                }

                // 添加到数组中
                result.add(tuple);
            }

        } catch (SocketTimeoutException e) {
            result.clear();

            System.out.println(e.getMessage());

            // 当捕获到超时异常的结果
            for (int i = 0; i < instructions.size(); ++i) {
                PLCRequestInstruction instruction = instructions.get(i);
                result.add(getErrorTuple(PLCDataStatus.PLC_DATA_STATUS_TIMEOUT, instruction));
            }

            e.printStackTrace();
            return result;
        } catch (ConnectException e) {
            result.clear();

            System.out.println(e.getMessage());

            // 当捕获到连接异常的结果
            for (int i = 0; i < instructions.size(); ++i) {
                PLCRequestInstruction instruction = instructions.get(i);
                result.add(getErrorTuple(PLCDataStatus.PLC_DATA_STATUS_CONNECT_ERROR, instruction));
            }

            e.printStackTrace();
            return result;
        } catch (IOException e) {
            result.clear();

            System.out.println(e.getMessage());

            // 当捕获到连接异常的结果
            for (int i = 0; i < instructions.size(); ++i) {
                PLCRequestInstruction instruction = instructions.get(i);
                result.add(getErrorTuple(PLCDataStatus.PLC_DATA_STATUS_ERROR, instruction));
            }

            e.printStackTrace();
            return result;
        } finally  // 关闭Socket
        {
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    /**
     * 用于生成指定错误类型的Tuple
     *
     * @param errorType   错误码
     * @param instruction 请求指令
     * @return PLCInstructionTuple
     */
    private PLCInstructionTuple getErrorTuple(int errorType, PLCRequestInstruction instruction) {
        PLCInstructionTuple errorTuple = new PLCInstructionTuple();
        errorTuple.requestInstruction = instruction;
        errorTuple.responseString = getConnectErrorResponse(instruction);
        errorTuple.responseState = PLCDataStatus.PLC_DATA_STATUS_ERROR;
        return errorTuple;
    }

    /**
     * 构造返回字节为-1时的指令
     *
     * @param ins 请求指令
     * @return 构造好的错误指令
     */
    private String getNullErrorResponse(PLCRequestInstruction ins) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("%");
        stringBuffer.append(ins.getLocationNumber_high());
        stringBuffer.append(ins.getLocationNumber_low());
        stringBuffer.append("!");
        stringBuffer.append(String.valueOf(PLCInstructionError.CORE_INNER_NO_RESPONSE_ERROR));
        stringBuffer.append("**");

        return stringBuffer.toString();
    }

    /**
     * 构造网络连接错误时返回的指令
     *
     * @param ins 请求指令
     * @return 构造好的错误指令
     */
    private String getConnectErrorResponse(PLCRequestInstruction ins) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("%");
        stringBuffer.append(ins.getLocationNumber_high());
        stringBuffer.append(ins.getLocationNumber_low());
        stringBuffer.append("!");
        stringBuffer.append(String.valueOf(PLCInstructionError.CORE_INNER_CONNECT_ERROR));
        stringBuffer.append("**");

        return stringBuffer.toString();
    }

    /**
     * 构造BBC错误时返回的指令
     *
     * @param ins 请求指令
     * @return 构造好的错误指令
     */
    private String getBBCErrorResponse(PLCRequestInstruction ins) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("%");
        stringBuffer.append(ins.getLocationNumber_high());
        stringBuffer.append(ins.getLocationNumber_low());
        stringBuffer.append("!");
        stringBuffer.append(String.valueOf(PLCInstructionError.BASIC_PROGRAM_BBC_ERROR));
        stringBuffer.append("**");
        return stringBuffer.toString();
    }

    /**
     * 通过流发送指令的方法
     *
     * @param dataOutputStream 输出流
     * @param dataInputStream  输入流
     * @param ins              要发送的指令
     * @return 返回的响应
     * @throws IOException
     */
    private String socket_Send(DataOutputStream dataOutputStream, DataInputStream dataInputStream, PLCRequestInstruction ins) throws IOException {
        String response = "";
        int tryTimes = 0; // 因BBC错误尝试的次数
        boolean emptyReturn = false; // 检测是否返回空指令

        while (true) {
            // 发送指令，刷新流
            System.out.println("即将发送的指令: " + ins.getInstructionString());
            dataOutputStream.writeUTF(ins.getInstructionString());
            dataOutputStream.flush();

            // 初始化字节ArrayList
            ArrayList<Byte> bytes = new ArrayList<Byte>();

            // 读取返回的数据
            System.out.println("开始流读取");
            while (true) {
                byte b;

                b = (byte) dataInputStream.read();
                bytes.add(b);

                if (b == 13) //
                {
                    emptyReturn = false;
                    break;
                }
                if (b == -1) // 如果返回的是-1空指令，则记录
                {
                    emptyReturn = true;
                    break;
                }
            }
            System.out.println("结束流读取");

            // 如果返回的是空指令
            if (emptyReturn) {
                response = getNullErrorResponse(ins);
                break;
            }

            // 初始化字节数组
            byte[] realBytes = new byte[bytes.size()];
            for (int i = 0; i < bytes.size(); ++i) {
                realBytes[i] = bytes.get(i);
            }

            // 获取响应信息
            response = new String(realBytes);

            // 检测BBC
            if (checkBBC(response)) {
                break;
            }

            System.out.println("fail BBC");
            ++tryTimes;
            if (tryTimes >= maxReadErrorTimes) // BBC错误超过一定次数后则取消发送，返回错误指令
            {
                response = getBBCErrorResponse(ins);
                break;
            }
        }

        System.out.println("本次返回结果: " + response);
        return response;
    }

    /**
     * 写指令的Fire_Ack发送方法
     *
     * @param dataOutputStream 数据输出流
     * @param dataInputStream  数据输入流
     * @param ins              需要发送的单条指令
     * @return 响应的指令
     * @throws IOException
     */
    String sendInstruction_fireAck(DataOutputStream dataOutputStream, DataInputStream dataInputStream, PLCRequestInstruction ins) throws IOException {
        PLCResponseInstruction ackResponse = null;
        PLCRequestInstruction ackRequest = null;
        if (ins instanceof PLCDataRegisterWriteInstruction) {
            PLCDataRegisterReadResponse checkResponse = new PLCDataRegisterReadResponse();
            checkResponse.setLocationNumber_high(ins.getLocationNumber_high());
            checkResponse.setLocationNumber_low(ins.getLocationNumber_low());
            checkResponse.setInstructionTypeCode(PLCResponseInstruction.PLCInstructionTypeResponseCode);
            checkResponse.setResponseData(((PLCDataRegisterWriteInstruction) ins).getWriteData());

            PLCDataRegisterReadInstruction checkRequest = new PLCDataRegisterReadInstruction();
            checkRequest.setLocationNumber_high(ins.getLocationNumber_high());
            checkRequest.setLocationNumber_low(ins.getLocationNumber_low());
            checkRequest.setDataCode(((PLCDataRegisterWriteInstruction) ins).getDataCode());
            checkRequest.setDataRegisterNumber(((PLCDataRegisterWriteInstruction) ins).getDataRegisterNumber());

            ackResponse = checkResponse;
            ackRequest = checkRequest;
        } else if (ins instanceof PLCSingleContactorWriteInstruction) {
            PLCSingleReadResponseInstruction checkResponse = new PLCSingleReadResponseInstruction();
            checkResponse.setLocationNumber_high(ins.getLocationNumber_high());
            checkResponse.setLocationNumber_low(ins.getLocationNumber_low());
            checkResponse.setInstructionTypeCode(PLCResponseInstruction.PLCInstructionTypeResponseCode);
            checkResponse.setContactorData(((PLCSingleContactorWriteInstruction) ins).getContactorData());

            PLCSingleContactorReadInstruction checkRequest = new PLCSingleContactorReadInstruction();
            checkRequest.setLocationNumber_high(ins.getLocationNumber_high());
            checkRequest.setLocationNumber_low(ins.getLocationNumber_low());
            checkRequest.setContactorCode(((PLCSingleContactorWriteInstruction) ins).getContactorCode());
            checkRequest.setContactorNumber(((PLCSingleContactorWriteInstruction) ins).getContactorNumber());

            ackResponse = checkResponse;
            ackRequest = checkRequest;
        } else if (ins instanceof PLCMultipleContactorWriteInstruction) {
            PLCMultipleReadResponse checkResponse = new PLCMultipleReadResponse();
            checkResponse.setLocationNumber_high(ins.getLocationNumber_high());
            checkResponse.setLocationNumber_low(ins.getLocationNumber_low());
            checkResponse.setInstructionTypeCode(PLCResponseInstruction.PLCInstructionTypeResponseCode);
            checkResponse.setResponseData(((PLCMultipleContactorWriteInstruction) ins).getWriteData());

            PLCMultipleContactorReadInstruction checkRequest = new PLCMultipleContactorReadInstruction();
            checkRequest.setLocationNumber_high(ins.getLocationNumber_high());
            checkRequest.setLocationNumber_low(ins.getLocationNumber_low());
            checkRequest.setCount(((PLCMultipleContactorInstruction) ins).getCount());
            checkRequest.setContactorCodes(((PLCMultipleContactorInstruction) ins).getContactorCodes());
            checkRequest.setContactorNumbers(((PLCMultipleContactorInstruction) ins).getContactorNumbers());

            ackResponse = checkResponse;
            ackRequest = checkRequest;
        }

        String response = socket_Send(dataOutputStream, dataInputStream, ins);

        if (ackResponse == null || response.charAt(3) == '!') {
            return response;
        }

        ackResponse.setBBC();
        String ack = socket_Send(dataOutputStream, dataInputStream, ackRequest);

        if (ack.equals(ackResponse.toString())) {
            return response;
        } else {
            PLCResponseInstruction errorResponse = new PLCResponseInstruction();
            errorResponse.setLocationNumber_high(ins.getLocationNumber_high());
            errorResponse.setLocationNumber_low(ins.getLocationNumber_low());
            errorResponse.setInstructionTypeCode(PLCResponseInstruction.PLCInstructionTypeErrorResponseCode);
            errorResponse.setResponseCode(String.valueOf(PLCInstructionError.CORE_INNER_CONFIRM_ERROR));
            return errorResponse.getInstructionString();
        }
    }

    /**
     * 在字符串的级别上通过BBC检查指令是否正确
     *
     * @param resposne 响应指令
     * @return 该指令是否正确
     */
    private boolean checkBBC(String resposne) {
        int result = 0;
        int length = resposne.length();
        for (int i = 0; i < length - 3; ++i) {
            char c = resposne.charAt(i);
            result ^= c;
        }

        String stringResult = PLCNumberTransform.HEXStringFromDECNumber(result);
        return stringResult.equals(resposne.substring(length - 3, length - 1));
    }

    public static void main(String[] args) throws Exception {

        Socket socket = new Socket("110.64.86.253", 9094);

        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
        DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

        String ins = "%01#RDD2000020001**" + "\r";
        dataOutputStream.writeUTF(ins);
        dataOutputStream.flush();

        // 初始化字节ArrayList
        ArrayList<Byte> bytes = new ArrayList<Byte>();

        // 读取返回的数据
        System.out.println("开始流读取");
        while (true) {
            byte b;

            b = (byte) dataInputStream.read();
            bytes.add(b);

            if (b == 13) //
            {
                break;
            }
            if (b == -1) // 如果返回的是-1空指令，则记录
            {
                break;
            }
        }
        System.out.println("结束流读取");

        // 初始化字节数组
        byte[] realBytes = new byte[bytes.size()];
        for (int i = 0; i < bytes.size(); ++i) {
            realBytes[i] = bytes.get(i);
        }

        // 获取响应信息
        String response = new String(realBytes);
        System.out.println(response);
    }
}
